package com.kamistoat.ruoyi.ruoyicommonsecurity.aspect;

import com.kamistoat.ruoyi.ruoyicommoncore.constant.SecurityConstants;
import com.kamistoat.ruoyi.ruoyicommoncore.exception.InnerAuthException;
import com.kamistoat.ruoyi.ruoyicommoncore.utils.ServletUtils;
import com.kamistoat.ruoyi.ruoyicommoncore.utils.StringUtils;
import com.kamistoat.ruoyi.ruoyicommonsecurity.annotation.InnerAuth;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

/**
 * 对被 @InnerAuth注解的内部方法的调用进行验证，要求请求来源为内部来源。使用AOP切面技术. <p></p>
 *
 * 使用 @Aspect注解，声明这个类将会别作为一个切面被容器织入到目标对象.
 * TODO AOP编程很重要，需要看相关知识学习。这里简单介绍 @Aspect 和 @Around的原理。 <p></p>
 *
 * 切面--一种预埋的接入点，允许容器在运行时的任意时刻访问这个接入点。
 * 切面可以简单理解成拦截器，被切面包裹的任何内容(不管是类/方法/注解)相当于被一个拦截器所包裹，在方法/类/注解被执行的前/后阶段，容器都可以对其进行拦截，并执行切面定义的动作。
 * 其中@Aspect注解就是用来声明该类为一个切面类，通常@Component会和他同时出现。<p></p>
 *
 * 单独使用@Ascept是没有任何效果的，因为它只是声明被注解的类会成为一个切面，换种理解就是声明被注解的类会成为一个拦截器。
 * 但是拦截器拦截哪些事件，拦截后执行哪些动作，这些需要在类内部使用 切点注解。
 * 即三大切点注解: @Around(环绕) @Before(前置) @After(后置)
 */
@Aspect
@Component
public class InnerAuthAspect implements Ordered {
    /**
     * 环绕切点 @Around，整个被代理的内容都会被@Around环绕，允许我们在方法执行前和方法执行后都做处理。<p></>
     *
     * 三种切点注解都可以使用属性 value 指定切点要代理的内容。<p></p>
     *
     * 例如 "@annotation(com.kamistoat.ruoyi.ruoyicommonsecurity.annotation.InnerAuth)" 意思是当要执行或访问的方法/类上带有 @InnerAuth注解时。<p></p>
     * 例如 "execution(* com.kamistoat.controller.*.*(..))" 意思是执行com.kamistoat.controller包下的任意类(对应第二个*)下的任意方法(对应第三个*和无限制的入参)时。
     *   注意第一个*不能省略。另外如果想要限制输入参数，只写参数类型即可，例 (String, String, Object). <p></p>
     *
     * P.S. 切点条件允许使用 || 和 && 逻辑，直接使用||拼接单独的条件即可，详见 PreAuthorizeAspect类 。<p></p>
     *
     * 作为切点方法的必需输入参数是 ProceedingJoinPoint，就是被拦截的切入点。调用 ProceedingJoinPoint.proceed() 执行被拦截的内容. <p></p>
     * 通常情况下，只需要该输入参数即可，关于切点处的详细信息，都可以通过 proceedingJoinPoint.getSignature() 获取到 MethodSignature / TypeSignature 之后，
     *   继续获取方法头/类头上的相关信息。例如获取方法头上的注解: MethodSignature.getAnnotation(xxxAnnotation.class)
     *
     * P.S. 如果切点注解的value为 "@annotation(xxx)" 的形式，有两种特殊情况:
     *   如果被 @Around()注解的方法的输入参数没有 `Ano ano`，则@Around中的ano必须写成绝对路径 @annotation(xxx.xxx.xxx.Ano)。
     *   如果被 @Around()注解的方法的输入参数有 `Ano ano`，则@Around中的ano必须写成和输入参数同名 @annotation(xxx.xxx.xxx.ano)。
     *   原因很简单，value接收String类型值，不明确指定元类，切点压根找不到
     *
     * @param proceedingJoinPoint 拦截接入点
     * @param innerAuth           @InnerAuth注解
     */
    @Around(value = "@annotation(innerAuth)")
    public Object innerAround(ProceedingJoinPoint proceedingJoinPoint, InnerAuth innerAuth) throws Throwable {
        /**
         * 这里验证 @InnerAuth 的逻辑倒是很简单，这是我们自定的。
         * 就是保证在调用 被@InnerAuth 标注的方法前，手动向请求Header中添加一个 k-v对: from_source - inner 。
         * 然后切面检查 被@InnerAuth 标注的方法接收的请求Header中是否包含上述k-v对。
         * 就是一个自定义的二次握手的过程，不是啥自带的高级东西。
         */
        // 从当前请求头中提取请求来源
        String source = ServletUtils.getHeader(SecurityConstants.FROM_SOURCE);
        // @InnerAuth 注解的方法必须得是内部调用
        if (!StringUtils.equals(SecurityConstants.INNER, source)) {
            throw new InnerAuthException("失败! 该方法只允许内部调用，当前请求不具备内部权限!");
        }
        // 获取请求头中的user信息
        String userId = ServletUtils.getHeader(SecurityConstants.DETAILS_USER_ID);
        String userName = ServletUtils.getHeader(SecurityConstants.DETAILS_USERNAME);
        // 用户是否匿名校验
        if (innerAuth.isUser() && (StringUtils.isEmpty(userId) || StringUtils.isEmpty(userName))) {
            throw new InnerAuthException("失败! 该方法只允许内部非匿名用户调用，当前请求由内部匿名用户发起!");
        }
        // 全部通过则执行拦截内容
        /**
         * 只要是包含前置环节的切面，都可以使用 ProceedingJoinPoint.proceed() 继续原始内容的执行。
         * proceed()方法有一个proceed(Object[] args)重载。即如果切面是切在一个方法上，可以使用其为方法重新传入自定义的输入参数。
         */
        Object proceedResult = proceedingJoinPoint.proceed();
        /**
         * @Around注解允许在执行完拦截内容后，继续某些处理，甚至可以修改上面的proceedResult的值后再返回
         */
        return proceedResult;
    }

    /**
     * 实现 Ordered接口的唯一方法。
     * 该切面拦截 需要在 PreAuthorizeAspect的 权限认证切面拦截前执行。
     * 因为在这儿通不过的请求，在权限认证肯定更通不过(外部用户无内部权限，匿名用户压根就没权限)。
     * 所以要在权限认证前，完成这里的拦截，提升系统性能。
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}




















